2023년 3월 30일
fastAPI 에 대해 간단하게 알아보았으며, 공식문서를 통해 fastAPI를 배워보겠습니다.
매개변수를 받아 반환할수 있고, 해당 매개변수에 타입을 지정할 수 있습니다.
from fastapi import FastAPI
app = FastAPI()
@app.get("/user/{id}")
async def user_id(id: int): # id 의 타입은 숫자형입니다.
return {"user_id": id}
만약 user/{id} 와 user/me를 함께 사용해야 할 경우
user/me함수를 먼저 선언해주어야 합니다.
from fastapi import FastAPI
app = FastAPI()
@app.get("/uses/me")
async def get_user_me(): # 해당함수가 먼저 선언되어야 합니다.
return {"user_id": "the current user"}
@app.get("/user/{id}")
async def user_id(id: int):
return {"user_id": id}
만약 매개변수값에 대해 고정적인 처리를 하고싶다면, enum클래스를 생성하여 고정적인 처리를 할수 있습니다.
from enum import Enum
from fastapi import FastAPI
class ModelName(str, Enum): # enum 클래스를 선언합니다.
alexnet = "alexnet" # TS 의 인터페이스와 타입을 함께 선언하는 느낌입니다.
resnet = "resnet"
lenet = "lenet"
app = FastAPI()
@app.get("/models/{model_name}")
async def get_model(model_name: ModelName): # 이경우 docs에서 ModelName클래스의 값이 정의되어 표시됩니다.
if model_name is ModelName.alexnet:
return {"model_name": model_name, "message": "Deep Learning FTW!"}
if model_name.value == "lenet":
return {"model_name": model_name, "message": "LeCNN all the images"}
return {"model_name": model_name, "message": "Have some residuals"}
경로 매개변수의 일부가 아닌 다른 함수 매개변수를 선언할 때, “쿼리” 매개변수로 자동 해석합니다.
쿼리 매개변수는 경로에서 고정된 부분이 아니기 때문에 선택적일 수 있고 기본값을 가질 수 있습니다.
from typing import Union
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: str, q: Union[str, None] = None, short: bool = False):
# q = None 로 기본값을 주었기 때문에 필수값이 아니며, short는 필수값입니다.
item = {"item_id": item_id}
if q:
item.update({"q": q})
if not short:
item.update(
{"description": "This is an amazing item that has a long description"}
)
return item
보통 GET 요청이 아닌, POST/DELETE/PUT 요청의 경우 리퀘스트 바디에 요청값을 보냅니다. (필수는 아닙니다)
이때 pydantic의 BaseModel을 이용하여, 데이터모델을 상속하는 클래스를 만듭니다.
from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: Union[str, None] = None # 필수값은 아닙니다.
price: float
tax: Union[float, None] = None # 필수값은 아닙니다.
# 파이썬 타입힌트만으로도 fastAPI는 다음과 같은 일을 수행합니다.
# 요청 본문을 JSON으로 읽습니다.
# 해당유형을 반환해줄 수 있습니다.
# 데이터를 검증합니다.
# 매개변수 및 파라미터에서 받은 데이터를 반환합니다.
app = FastAPI()
@app.post("/items/{item_id}")
async def create_item(item_count:int, item: Item): # 매개변수값을 반환값에 포함합니다.
item_dict = item.dict()
if item.tax:
price_with_tax = (item.price + item.tax)*item_count
item_dict.update({"price_with_tax": price_with_tax})
return item_dict
from typing import Union
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/")
async def read_items(q: Union[str, None] = None):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
위와 같은 코드에서 만약 매개변수 ‘q’에 대해 문자열이 50자를 초과하지 않도록 제한하고 싶다면 어떻게 해야 할까요?
from fastapi import FastAPI, Query
from typing_extensions import Annotated
...
# 매개변수 q에 타입을 Annotated으로 감싸줍니다.
async def read_items(q: Annotated[Union[str, None], Query(max_length=50)] = None):
# 또한 다음과 같습니다.
async def read_items(q: Union[str, None] = Query(default=None, max_length=50)):
# 다음과 같으며 가장 권장하는 방식입니다.
async def read_items(q: Annotated[str, Query(max_length=50)]):
# 해당 어노테이티드로 매개변수 'q'에 문자열 길이를 제한할 수 있습니다.
# max_length 말고도 정규식등 다양한 옵션설정이 가능합니다.
# 해당값이 필수값인경우 리터럴을 이용해 표현할 수 있습니다.
async def read_items(q: Annotated[str, Query(min_length=3)] = ...):
# 물론 Required를 이용해 표현할 수 있습니다.
from pydantic import Required
from typing_extensions import Annotated
...
async def read_items(q: Annotated[str, Query(min_length=3)] = Required):
문자열뿐 아니라 숫자형에 대해서도 유효성 검사를 할 수 있습니다.
from fastapi import FastAPI, Path
app = FastAPI()
# ge 는 크거나 같다는 뜻으로 item_id 가 1보다 크거나 같아야 한다는 뜻입니다.
# 외에 gt,ge,lt,le 가 있습니다.
@app.get("/items/{item_id}")
async def read_items(
*, item_id: int = Path(title="The ID of the item to get", ge=1), q: str
):
results = {"item_id": item_id}
if q:
results.update({"q": q})
return results
파이썬은 버전 3.4부터 다른 언어들 처럼 enum(enumeration, 이넘) 타입을 지원하고 있습니다.
enum은 서로 관련이 있는 상수의 집합을 정의할 때 사용됩니다.
# 일반적으로 클래스에 상속하여 사용합니다.
# key와 value쌍으로 만들수 있습니다.
from enum import Enum
class Skill(Enum):
HTML = 1
CSS = 2
JS = 3
# 자주 사용하지는 않지만 함수형으로도 정의할수 있습니다.
>>> Skill = Enum("Skill", "HTML CSS JS")
>>> list(Skill)
[<Skill.HTML: 1>, <Skill.CSS: 2>, <Skill.JS: 3>]
# 또한 auto()를 통해 숫자 1부터 자동으로 value에 할당할 수 있습니다.
from enum import Enum, auto
class Skill(Enum):
HTML = auto()
CSS = auto()
JS = auto()
# enum내의 _generate_next_value_ 매서드를 오버라이드해서 다른값을 자동으로 할당할수 있습니다.
# name 의 경우 key값과 같은 value값을 가질 수 있습니다.
from enum import Enum, auto
class Skill(Enum):
def _generate_next_value_(name, start, count, last_values):
return name
HTML = auto()
CSS = auto()
JS = auto()
# enum을 사용할 때 name이나 value를 사용해야 반드시 제대로된 key,value값이 나오는데,
# mixin을 이용해 str을 확장하는 클래스를 만들고, 해당 클래스를 상속해 사용할 enum클래스를 만듭니다.
# 아래와같이 enum또한 다른 클래스처럼 상속과 확장이 가능합니다.
class StrEnum(str, Enum):
def _generate_next_value_(name, start, count, last_values):
return name # value값을 key값과 같게끔 이름을 반환합니다.
def __repr__(self):
return self.name
def __str__(self):
return self.name
class Skill(StrEnum):
HTML = auto()
CSS = auto()
JS = auto()
>>> Skill.HTML == 'HTML'
True
>>> isinstance(Skill.HTML, str)
True
# 근데 이렇게 쓸 바에야 차라리 함수로 상수를 리턴한다는 의견도 있다니, 취향껏 쓰시면 되지않을까 싶습니다.
def get_const():
return 1000
>>> print(get_const()) # 1000
# 참고로 상수에 대한 타입인 파이널도 있습니다.
from typing import Final
const: Final[float] = 1000
파이썬의 빌트인되어있는 str,int,bool만으로는 타입표기의 어려움이 있습니다.
그래서 파이썬의 3.5버전 부터 타입 어노테이션을 위해 사용합니다.
다음과 같은 타입을 명시할 수 있습니다.
# 1. Union : 여러개의 타입을 받을수 있습니다.
from typing import Union
def process_message(msg: Union[str, bytes, None]) -> str:
# 2. Optional : Union으로 str 혹은 None일경우 대체할수 있습니다.
from typing import Optional
def eat_food(food: Optional[str]) -> None:
# 파이썬 3.10부터는 | 을 사용할 수 있습니다.
# 3. List, Tuple, Dict : 각각 배열, 튜블, 딕셔너리를 표기하며, 딕셔너리의 경우 key,value 형태로 대괄호 안의 정의합니다.
from typing import List, Tuple, Dict
names: List[str]
location: Tuple[int, int, int]
coount_map: Dict[str, int]
# 4. TypedDict : 딕셔너리의 키밸류쌍이 2개이상일 경우 사용합니다. 클래스로 상속받아 나머지 딕셔너리 키,밸류쌍을 정의합니다.
from typing import TypedDict
class Person(TypedDict):
name: str
age: int
gender: str
def calc_cost(person: Person) -> float:
# 또는 아래와같이 사용이 가능합니다.
Person = TypedDict("Person", name=str, age=str, gender=str)
Person = TypedDict("Person", "name": str, "age": int, "gender": str})
# 또한 dataclass로 대체하여 사용이 가능합니다.
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
gender: str
def calc_cost(person: Person) -> float:
# 5. Generator, Iterable, Iterator : 어떤 함수가 제너레이터의 역할을 한다면 리턴값에 Generator[YieldType, SendType, ReturnType]을 사용합니다.
def echo_round() -> Generator[int, float, str]:
sent = yield 0
while sent >= 0:
sent = yield round(sent)
return 'Done'
# send값과 retrun값이 없이 yield만 선언한 경우 Generator[YieldType, None, None]으로 사용하거나 다음과 같습니다.
import random
from typing import Iterator
def random_generator(val: int) -> Iterator[float]:
for i in range(val):
yield i
# 참고로 Iterator와 Iterable는 구분할 필요가 없으며, _next_의 존재 여부에 따라 구분합니다.
# 6. Callable : 함수를 인자로 가지는 경우에는 Callable[[Arg1Type, Arg2Type], ReturnType]으로 사용합니다.
from typing import Callable
def on_some_event_happened(callback: Callable[[int, str, str], int]) -> None:
# 만약 인자의 타입을 신경쓰지 않고 리턴값을 반환하는 함수를 표현하고 싶을때는 아래와 같이 표기합니다.
def on_some_event_happened(callback: Callable[..., float], *args: int) -> None:
# 단, 인자가 아예 없는것을 원할 경우 빈 값을 전달합니다.
def on_some_event_happened(callback: Callable[[], float], *args: int) -> None:
# 7. Type : 일반적으로 클래스의 객체는 해당타입을 명시하나, 클래스 그자체를 인자로 받을때는 Type[클래스명]으로 사용합니다.
class Factory:
...
class AFactory(Factory):
...
class BFactory(Factory):
...
def initiate_factory(factory: Type[Factory]):
# 예외를 받는 경우에도 아래와 같습니다.
def on_exception(exception_class: Type[Exception]):
# 8. Any : 어떤 타입이든 전부 받을수 있습니다.
# 9. TypeVar : 제너릭 타입을 표시할 경우 사용합니다.
from typing import Sequence, TypeVar, Iterable
T = TypeVar("T") # T 대신 다른 문자/단어를 써도 되지만 일반적으로 T를 사용합니다.
def batch_iter(data: Sequence[T], size: int) -> Iterable[Sequence[T]]:
for i in range(0, len(data), size):
yield data[i:i + size]
# 이 경우 Sequence[T]에는 다양한 타입이 올수 있습니다.
# 만약 이중 특정 타입만을 제한하고 싶다면 아래와 같이 사용합니다.
T = TypeVar("T", bound=Union[int, str, bytes])
# 이 경우 int, str, bytes 또는 해당 타입을 상속한 타입으로 제한됩니다.
# 예시를 보자면 아래의 경우는 float타입이기 때문에 실패하지만
batch_iter([1.1, 1.3, 2.5, 4.2, 5.5], 2)
# 이경우 int를 상속받았기 때문에 유효성검사를 통과합니다.
class SomeInteger(int):
pass
batch_iter([SomeInteger(1), SomeInteger(2.5), SomeInteger(3.3)], 2)
# Union과의 차이점은 리턴 타입 Iterable[Sequence[T]]와 인풋 타입 Sequence[T]가 의존관계에 있다는 것입니다. 인풋타입 = 리턴타입
# 이외에도 제너릭 클래스를 만들기위헤 Generic을 사용하기도 합니다.
# 10. ParamSpec : Callable타입의 콜백 함수들을 인자로 가지는 경우에는 해당 콜백 함수의 인자의 타입에 이를 호출하는 함수가 의존하는 경우가 있습니다
from typing import TypeVar, Callable, ParamSpec
import logging
T = TypeVar('T')
P = ParamSpec('P') # 사실 제네릭타입의 일종일수 있습니다.
def add_logging(f: Callable[P, T]) -> Callable[P, T]:
'''A type-safe decorator to add logging to a function.'''
def inner(*args: P.args, **kwargs: P.kwargs) -> T:
logging.info(f'{f.__name__} was called')
return f(*args, **kwargs)
return inner
@add_logging
def add_two(x: float, y: float) -> float:
'''Add two numbers together.'''
return x + y
@add_logging
def send_msg(msg: str) -> None:
print(f"I Sent {msg}")
# 11. Protocol : 클래스내의 함수를 정의하고 해당 행동에 대한 타입을 명시합니다. 이는 다른언어에서 인터페이스라고 합니다.
# 만약 다음과 같의 정의하고 각각 기쁜일과 화날일을 호출합니다.
from typing import Protocol
class 감정(Protocol):
def 기쁘다(self) -> str:
...
def 슬프다(self) -> str:
...
class 사람:
def 기쁘다(self) -> str:
return "기뻐!"
def 화나다(self) -> str:
return "화나!"
class 사회생활:
def 시작(self, 사람: 감정) -> None:
self.사람 = 사람
def 기쁜일(self) -> str:
return self.사람.기쁘다()
def 화날일(self) -> str:
return self.사람.화나다()
갓생 = 사회생활()
갓생.시작(사람())
print(갓생.기쁜일())
print(갓생.화날일())
# 그렇게 한다면 각각 "기뻐"와 "화나"가 각각 출력됩니다.
# 하지만 우리는 감정에는 화나다라는 매서드를 정의하지 않았습니다.
>>> 기뻐!
>>> 화나!
# 해당 이유는 Protocol은 ABC 추상 메서드와 다르게 런타임 체크를 하지 않기 때문입니다.
# 만약 추상매서드를 상속받아 실행했다면 "슬프다"라는 매서드가 없기때문에 오류를 뱉었을것입니다.
# 대신 mypy같은 정적검사툴을 이용해 정적검사를 할 수있습니다.
error: "감정" has no attribute "화나다"
error: Argument 1 to "시작" of "사회생활" has incompatible type "사람"; expected "감정"
note: "사람" is missing following "감정" protocol member:
note: 슬프다